Skip to content

feat(@schematics/angular): add refactor-fake-async migration#32784

Draft
yjaaidi wants to merge 7 commits intoangular:mainfrom
yjaaidi:feat/add-refactor-fake-async-migration
Draft

feat(@schematics/angular): add refactor-fake-async migration#32784
yjaaidi wants to merge 7 commits intoangular:mainfrom
yjaaidi:feat/add-refactor-fake-async-migration

Conversation

@yjaaidi
Copy link
Contributor

@yjaaidi yjaaidi commented Mar 17, 2026

PR Checklist

Please check to confirm your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

There is no migration from fakeAsync to Vitest fake timer APIs.

Issue Number: N/A

What is the new behavior?

The current migration transforms the following test:

it("should work", fakeAsync(() => {
  let value = 0;

  setTimeout(() => value = 42, 100);

  tick(100);
  flush();

  expect(value).toBe(42);
}));

into:

import { onTestFinished, vi } from 'vitest';

it("should work", async () => {
  vi.useFakeTimers();
  onTestFinished(() => {
    vi.useRealTimers();
  });  

  let value = 0;

  setTimeout(() => value = 42, 100);

  await vi.advanceTimersByTimeAsync(100);
  await vi.runAllTimersAsync();

  expect(value).toBe(42);
});

Does this PR introduce a breaking change?

  • Yes
  • No

Open Questions

  • This migration could be part of the jasmine-vitest migration. Should I moved it there and add an opt-out?
  • flushMicrotasks can only be partially reproduced (i.e. explicit queueMicrotask calls), transforming it have high chances of producing a broken test. Should we just skip transforming tests that use it?
  • Should we generate code that measures time "manually" when the return value of flush is used?
  • Should we generate an "ad-hoc" function in the test file that reproduces flush's maxTurns option?
  • What should we do about processNewMacroTasksSynchronously option?
  • This only migrates flush and tick if used inside fakeAsync but it's probably better to just replace in the whole file as users might create some wrappers. What do you think?

@yjaaidi yjaaidi marked this pull request as draft March 17, 2026 16:04
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new experimental schematic, refactor-fake-async, to migrate Angular's fakeAsync tests to use Vitest's fake timer APIs. The changes include the schematic implementation, transformers for fakeAsync, tick, and flush, and corresponding tests. Additionally, the findTestFiles utility function has been refactored into a shared location.

My review focuses on improving the robustness of the new transformers. I've suggested changes to ensure that the necessary vitest imports are always added when tick() or flush() are transformed, which will prevent potential issues with broken code after migration.

Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This migration could be part of the jasmine-vitest migration. Should I moved it there and add an opt-out?

Makes sense to me.

  • flushMicrotasks can only be partially reproduced (i.e. explicit queueMicrotask calls), transforming it have high chances of producing a broken test. Should we just skip transforming tests that use it?

Hmm, wouldn't await vi.advanceTimersByTimeAsync(0) be close enough?

  • Should we generate code that measures time "manually" when the return value of flush is used?

Like with Date? I suppose we could and that'd get us there. Do people really use the return value....? Maybe just add a TODO if people ask for it.

  • Should we generate an "ad-hoc" function in the test file that reproduces flush's maxTurns option?

like with fakeTimers.loopLimit? Maybe this can also be a TODO and we just see if people really need it.

  • What should we do about processNewMacroTasksSynchronously option?

Kill it 🔥

  • This only migrates flush and tick if used inside fakeAsync but it's probably better to just replace in the whole file as users might create some wrappers. What do you think?

Yea, I had a comment about this and am seeing this note now. Probably should migrate it. They need to be in a fakeAsync somewhere up the chain.


const newContent = await transformContent(`
it("should work", () => {
flush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't flush be in a helper method? if the flush is the one from @angular/core/testing I would think it should be transformed since it would throw if it wasn't in fakeAsync somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This answers one on two subsequent questions:

  • shouldn't we only transform identifiers if source is @angular/core/testing? I assume that we should as there are higher chances of name collisions than users importing the real tick / flush through a custom barrel module (e.g. import { tick, flush } from 'my-custom-barrel-that-reexports-angular-core-testing')
  • no need to handle things like aliased imports of tick and flush, right? Sounds too much of an edge-case to me.

@yjaaidi
Copy link
Contributor Author

yjaaidi commented Mar 18, 2026

  • This migration could be part of the jasmine-vitest migration. Should I moved it there and add an opt-out?

Makes sense to me.

👌 It's also much less effort than having to factorize 50% of what jasmine-vitest migration is doing in terms of test file collection etc...

  • flushMicrotasks can only be partially reproduced (i.e. explicit queueMicrotask calls), transforming it have high chances of producing a broken test. Should we just skip transforming tests that use it?

Hmm, wouldn't await vi.advanceTimersByTimeAsync(0) be close enough?

It's the closest thing we can get.
From the usage examples I found on github (such as on angular/components repo), I didn't notice any test where executing the 0ms timers in addition to the microtask queue would cause a side effect that would break the test.

  • Should we generate code that measures time "manually" when the return value of flush is used?

Like with Date? I suppose we could and that'd get us there. Do people really use the return value....? Maybe just add a TODO if people ask for it.

👌

  • Should we generate an "ad-hoc" function in the test file that reproduces flush's maxTurns option?

like with fakeTimers.loopLimit? Maybe this can also be a TODO and we just see if people really need it.

I was thinking of generating a function such as this one in every test file that is using the maxTurns option.

async function flush(maxTurns: number) {
  let count = 0;
  while (vi.getTimerCount() > 0) {
    ++count;
    if (count > maxTurns) {
     throw ...
    }
    await vi.advanceTimersToNextTimerAsync();
  }
}

I'll go with the TODO 😊

  • What should we do about processNewMacroTasksSynchronously option?

Kill it 🔥

☠️

  • This only migrates flush and tick if used inside fakeAsync but it's probably better to just replace in the whole file as users might create some wrappers. What do you think?

Yea, I had a comment about this and am seeing this note now. Probably should migrate it. They need to be in a fakeAsync somewhere up the chain.

👌

Perfect! Thank you very much for the review.
You've answered all my questions 😊.
I'll take care of this as soon as I can.

@yjaaidi
Copy link
Contributor Author

yjaaidi commented Mar 18, 2026

One last thing.
What do you think of replacing flushMicrotasks() with vi.advanceTimersToNextTimerAsync() instead of vi.advanceTimersByTimeAsync(0)? It's a tiny bit closer to it.

@atscott
Copy link
Contributor

atscott commented Mar 19, 2026

One last thing.
What do you think of replacing flushMicrotasks() with vi.advanceTimersToNextTimerAsync() instead of vi.advanceTimersByTimeAsync(0)? It's a tiny bit closer to it.

maybe I’m misremembering what that does but isn’t it not closer? advanceTimersToNextTimerAsync Would go all the way to the next timer, potentially to 5000 if that’s where the next one is rather than only flushing tasks at 0.

@yjaaidi
Copy link
Contributor Author

yjaaidi commented Mar 19, 2026

maybe I’m misremembering what that does but isn’t it not closer? advanceTimersToNextTimerAsync Would go all the way to the next timer, potentially to 5000 if that’s where the next one is rather than only flushing tasks at 0.

My bad! I should have gone to bed instead of writing my previous comment 😅.
I made two wrong assumptions:

From a fresh morning perspective, I think that the closest thing to flushMicrotasks would be:

import { getSafeTimers } from '@vitest/utils/timers';

async function flushMicrotasks() {
  return new Promise((resolve) => {
    const { setTimeout: safeSetTimeout } = getSafeTimers();
    safeSetTimeout(resolve);
  }); 
}

The drawbacks I see are:

  • having to add @vitest/utils as a dependency
  • the boilerplate of generating the flushMicrotasks utility in each test that needs it

Unless! Unless it makes sense for vitest to add vi.flushMicrotasks() 🤔

But back to the usages of flushMicrotasks that I've observed, vi.advanceTimersByTimeAsync(0) works.
So pragmatically speaking, it could be the way to go and we keep the — bit more realistic — flushMicrotasks as a TODO for later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants